package com.openass.io
{
   import flash.display.Stage;
   import flash.events.Event;
   import flash.events.KeyboardEvent;
   import flash.events.MouseEvent;
   import flash.ui.Keyboard;
   import flash.utils.Dictionary;
   
   /**
    * A manager to handle Keyboard and Mouse events. Example:
    * </br></br><code>
    * Input.api.init(stage);</br>
    * 
    * Input.api.dispose();</br>
    * 
    * Input.api.keyDown(Keyboard.SHIFT, Keyboard.A);</br>
    * 
    * Input.api.keyUp(Keyboard.B);</br>
    * 
    * Input.api.keyPress(Keyboard.C);</br>
    * 
    * Input.api.setKeyDownCallback(yourHandlerFunc);</br>
    * 
    * Input.api.setKeyUpCallback(yourHandlerFunc);</br>
    * </code>
    * @author boy, cjboy1984@gmail.com
    * @date 2012/05/05
    */
   public class Input
   {
      /**
       * Listen for this event on the stage for better mouse-up handling. This event will fire either on a legitimate mouseUp or when flash no longer has any idea what the mouse is doing.
       */
      protected static const MOUSE_UP_OR_LOST:String = 'MOUSE_UP_OR_LOST';
      
      /**
       * Mouse stuff.
       */
      public var isMouseDown:Boolean = false;
      
      /// Keyboard stuff. For these dictionaries, the keys are keyboard key-codes. The value is always true (a nil indicates no event was caught for a particular key).
      protected var keysDown:Dictionary;
      protected var keysPressed:Dictionary;
      protected var keysUp:Dictionary;
      
      protected var stage:Stage;
      
      // callback
      protected var keyDownCallback:Function;
      protected var keyUpCallback:Function;
      
      // singleton
      protected static var instance:Input;
      
      public function Input(pvt:PrivateClass)
      {
         // DO NOTHING
      }
      
      // ________________________________________________
      //                                             init
      
      /**
       * In order to track input, a reference to the stage is required. Pass the stage to this static function to start tracking input.</br>
       * NOTE: "clear()" must be called each timestep. If false is pased for "autoClear", then "clear()" must be called manually. Otherwise a low priority enter frame listener will be added to the stage to call "clear()" each timestep.
       */
      public function init(s:Stage, autoClear:Boolean = false):void
      {
         keysDown = new Dictionary();
         keysPressed = new Dictionary();
         keysUp = new Dictionary();
         
         if (autoClear)
         {
            s.addEventListener(Event.ENTER_FRAME, onEF, false, -1000, true); /// Very low priority.
         }
         
         s.addEventListener(KeyboardEvent.KEY_DOWN, onKDown, false, 0, true);
         s.addEventListener(KeyboardEvent.KEY_UP, onKUp, false, 0, true);
         
         s.addEventListener(MouseEvent.MOUSE_UP, onMUp, false, 0, true);
         s.addEventListener(MouseEvent.MOUSE_DOWN, onMDown, false, 0, true);
         s.addEventListener(Event.MOUSE_LEAVE, onMLeave, false, 0, true);
         s.addEventListener(Event.DEACTIVATE, onDeactivate, false, 0, true);
         
         stage = s;
      }
      
      /**
       * Dispose every thing about tracking input.
       */
      public function dispose():void
      {
         keysDown = null;
         keysPressed = null;
         keysUp = null;
         
         isMouseDown = false;
         
         if (stage)
         {
            stage.removeEventListener(Event.ENTER_FRAME, onEF);
            stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKDown);
            stage.removeEventListener(KeyboardEvent.KEY_UP, onKUp);
            
            stage.removeEventListener(MouseEvent.MOUSE_UP, onMUp);
            stage.removeEventListener(MouseEvent.MOUSE_DOWN, onMDown);
            stage.removeEventListener(Event.MOUSE_LEAVE, onMLeave);
            stage.removeEventListener(Event.DEACTIVATE, onDeactivate);
            
            stage = null;
         }
      }
      
      /**
       * clear key up and key pressed dictionaries.
       */
      public function clear():void
      {
         keysUp = new Dictionary();
         keysPressed = new Dictionary();
      }
      
      /**
       * callback function must correspond to the following prototype:</br>
       * <code>
       * ??? function FUNC_NAME(e:KeyboardEvent):void
       * </code>
       */
      public function setKeyDownCallback(callback:Function):void
      {
         keyDownCallback = callback;
      }
      
      /**
       * callback function must correspond to the following prototype:</br>
       * <code>
       * ??? function FUNC_NAME(e:KeyboardEvent):void
       * </code>
       */
      public function setKeyUpCallback(callback:Function):void
      {
         keyUpCallback = callback;
      }
      
      // ________________________________________________
      //                               keyDown, Up, Press
      
      /**
       * Quick key-down detection for one or more keys. Pass uint that correspond to constants in the Keyboard class. If all of the passed keys are down, returns true. Example:
       * </br><code>
       * Input.api.keyDown(Keyboard.SHIFT, Keyboard.A); /// True if shift and a are currently down.
       * </code>
       */
      public function keyDown(...args):Boolean
      {
         return keySearch(keysDown, args);
      }
      
      /**
       * Quick key-up detection for one or more keys. Pass uint that correspond to constants in the Keyboard class. If all of the passed keys have been released this frame, returns true. Example:
       * </br><code>
       * Input.api.keyUp(Keyboard.SHIFT, Keyboard.A); /// True if shift and a are currently up.
       * </code>
       */
      public function keyUp(...args):Boolean
      {
         return keySearch(keysUp, args);
      }
      
      /**
       * Quick key-pressed detection for one or more keys. Pass uint that correspond to constants in the Keyboard class. If any of the passed keys have been pressed this frame, returns true. This differs from keyDown in that a key held down will only return true for one frame. Example:
       * </br><code>
       * Input.api.keyPress(Keyboard.SHIFT, Keyboard.A); /// True if shift and a are currently press.
       * </code>
       */
      public function keyPressed(...args):Boolean
      {
         return keySearch(keysPressed, args);
      }
      
      // ################### protected ##################
      
      /**
       * Used internally by keyDown(), keyUp() and keyPress().
       */
      protected function keySearch(d:Dictionary, keys:Array):Boolean
      {
         var ret:uint = 0;
         for (var i:uint = 0; i < keys.length; ++i)
         {
            if (d[keys[i]])
            {
               ++ret;
            }
         }
         
         return ret == keys.length;
      }
      
      // ________________________________________________
      //                                         keyboard
      
      /**
       * Record a key down, and count it as a key press if the key isn't down already.
       */
      protected function onKDown(e:KeyboardEvent):void
      {
         keysPressed[e.keyCode] = true;
         keysDown[e.keyCode] = true;
         
         // callback
         if (keyDownCallback as Function)
         {
            keyDownCallback(e);
         }
      }
      
      /**
       * Record a key up.
       */
      protected function onKUp(e:KeyboardEvent):void
      {
         keysUp[e.keyCode] = true;
         delete keysDown[e.keyCode];
         
         // callback
         if (keyUpCallback as Function)
         {
            keyUpCallback(e);
         }
      }
      
      /**
       * clear key up and key pressed dictionaries. This event handler has a very low priority, so it should
       * occur AFTER ALL other enterFrame events. This ensures that all other enterFrame events have access to
       * keysUp and keysPressed before they are cleared.
       */
      protected function onEF(e:Event):void
      {
         clear();
      }
      
      // ________________________________________________
      //                                            mouse
      
      /**
       * Record a mouse down event.
       */
      protected function onMDown(e:MouseEvent):void
      {
         isMouseDown = true;
      }
      
      /**
       * Record a mouse up event. Fires a MOUSE_UP_OR_LOST event from the stage.
       */
      protected function onMUp(e:MouseEvent):void
      {
         isMouseDown = false;
      }
      
      /**
       * The mouse has left the stage and is no longer trackable. Fires a MOUSE_UP_OR_LOST event from the stage.
       */
      protected function onMLeave(e:Event):void
      {
         isMouseDown = false;
      }
      
      /**
       * Flash no longer has focus and has no idea where the mouse is. Fires a MOUSE_UP_OR_LOST event from the stage.
       */
      protected function onDeactivate(e:Event):void
      {
         isMouseDown = false;
      }
      
      // ________________________________________________
      //                                         sinleton
      
      public static function get api():Input
      {
         if (!instance)
         {
            instance = new Input(new PrivateClass);
         }
         return instance;
      }
      
      // #################### private ###################
      
      // --------------------- LINE ---------------------
      
   }
   
}

class PrivateClass
{
   function PrivateClass()
   {
   }
}